<?php
/**
 * Created by PhpStorm.
 * User: whwyy
 * Date: 2018/3/30 0030
 * Time: 15:23
 */

namespace BeReborn\Database;


use BeReborn\Base\Component;
use BeReborn\Core\DateFormat;
use BeReborn\Core\JSON;
use BeReborn\Error\Logger;
use BeReborn\Exception\DbException;
use BeReborn\Http\Context;
use Exception;
use PDO;
use PDOStatement;

/**
 * Class Command
 * @package BeReborn\Database
 */
class Command extends Component
{
    const ROW_COUNT    = 'ROW_COUNT';
    const FETCH        = 'FETCH';
    const FETCH_ALL    = 'FETCH_ALL';
    const EXECUTE      = 'EXECUTE';
    const FETCH_COLUMN = 'FETCH_COLUMN';

    /** @var Connection */
    public $db;

    /** @var string */
    public $sql = '';

    /** @var array */
    public $params = [];

    /** @var string */
    private $_modelName;

    /** @var PDOStatement */
    private $prepare;

    /** @var PDO $connection */
    private $connection;


    /**
     * @param $executeSql
     * @param false $isSearch
     * @return array
     * @throws DbException
     */
    private function executedSql($executeSql, $isSearch = false)
    {
        if (empty($executeSql)) {
            throw new Exception('sql 不能为空~');
        }
        $time = microtime(true);
        if (($connect = $this->db->getConnect($executeSql)) === false) {
            throw new DbException('数据库繁忙, 请稍后再试!');
        }
        if (microtime(true) - $time > 0.02) {
            #$this->warning('get connect time:' . $executeSql . ';  ' . (microtime(true) - $time));
        }
        if ($isSearch === true) {
            $prepare = $connect->query($executeSql);
        } else {
            $prepare = $connect->prepare($executeSql);
            $this->bind($prepare);
        }
        if (!($prepare instanceof PDOStatement)) {
            throw new Exception($executeSql . ':' . $this->getError($prepare));
        }
        return [$connect, $prepare];
    }

    /**
     * @return bool|PDOStatement
     * @throws
     */
    public function incrOrDecr()
    {
        return $this->execute(static::EXECUTE);
    }

    /**
     * @param bool $isInsert
     * @param bool $hasAutoIncrement
     * @return bool|string
     * @throws
     */
    public function save($isInsert = TRUE, $hasAutoIncrement = true)
    {
        return $this->execute(static::EXECUTE, $isInsert, $hasAutoIncrement);
    }

    /**
     * @param $model
     * @param $attributes
     * @param $condition
     * @param $param
     * @return Command
     * @throws Exception
     */
    public function update($model, $attributes, $condition, $param)
    {
        $change = $this->db->getSchema()->getChange();
        $sql = $change->update($model, $attributes, $condition, $param);
        return $this->setSql($sql)->bindValues($param);
    }


    /**
     * @param $tableName
     * @param $attributes
     * @param $condition
     * @return Command
     * @throws Exception
     */
    public function batchUpdate($tableName, $attributes, $condition)
    {
        $change = $this->db->getSchema()->getChange();
        [$sql, $param] = $change->batchUpdate($tableName, $attributes, $condition);
        return $this->setSql($sql)->bindValues($param);
    }


    /**
     * @param $tableName
     * @param $attributes
     * @return Command
     * @throws Exception
     */
    public function batchInsert($tableName, $attributes)
    {
        $change = $this->db->getSchema()->getChange();
        $attribute_key = array_keys(current($attributes));
        [$sql, $param] = $change->batchInsert($tableName, $attribute_key, $attributes);
        return $this->setSql($sql)->bindValues($param);
    }

    /**
     * @param $tableName
     * @param $attributes
     * @param $param
     * @return Command
     * @throws Exception
     */
    public function insert($tableName, $attributes, $param)
    {
        $change = $this->db->getSchema()->getChange();
        $sql = $change->insert($tableName, $attributes, $param);
        return $this->setSql($sql)->bindValues($param);
    }

    /**
     * @return bool|int
     * @throws Exception
     */
    public function all()
    {
        return $this->execute(static::FETCH_ALL);
    }

    /**
     * @param $tableName
     * @param $param
     * @param $condition
     * @return Command
     * @throws Exception
     */
    public function mathematics($tableName, $param, $condition)
    {
        $change = $this->db->getSchema()->getChange();
        $sql = $change->mathematics($tableName, $param, $condition);
        return $this->setSql($sql);
    }

    /**
     * @return array|mixed
     * @throws Exception
     */
    public function one()
    {
        return $this->execute(static::FETCH);
    }

    /**
     * @return bool|int
     * @throws Exception
     */
    public function fetchColumn()
    {
        return $this->execute(static::FETCH_COLUMN);
    }

    /**
     * @return bool|int
     * @throws Exception
     */
    public function rowCount()
    {
        return $this->execute(static::ROW_COUNT);
    }

    /**
     * @return bool|int
     * @throws Exception
     */
    public function flush()
    {
        return $this->execute(static::EXECUTE);
    }

    /**
     * @param $type
     * @param $isInsert
     * @param $hasAutoIncrement
     * @return bool|int
     * @throws Exception
     */
    private function execute($type, $isInsert = null, $hasAutoIncrement = true)
    {
        if ($type === static::EXECUTE) {
            return $this->insert_or_change($isInsert, $hasAutoIncrement);
        } else {
            return $this->search($type);
        }
    }

    /**
     * @param $type
     * @return array|int|mixed
     * @throws Exception
     */
    private function search($type)
    {
        try {
            $time = microtime(true);
            /** @var PDOStatement $prepare */
            [$_, $prepare] = $this->executedSql($this->sql, true);
            if ($type === static::ROW_COUNT) {
                $result = $prepare->rowCount();
            } else if ($type === static::FETCH_COLUMN) {
                $result = $prepare->fetchColumn();
            } else if ($type === static::FETCH_ALL) {
                $result = $prepare->fetchAll(PDO::FETCH_ASSOC);
            } else {
                $result = $prepare->fetch(PDO::FETCH_ASSOC);
            }
            $prepare->closeCursor();
            $this->dumpExecuteTime($time);

            return $result;
        } catch (\Throwable $exception) {
            $result = $this->addError($this->sql . '. error: ' . $exception->getMessage());
            if (str_contains($exception->getMessage(), 'MySQL server has gone away')) {
                $this->db->refresh($this->sql);
                sleep(1);
                return $this->search($type);
            }
            return $result;
        } finally {
            $this->db->release();
        }
    }

    /**
     * @param $isInsert
     * @param $hasAutoIncrement
     * @return bool|string
     * @throws Exception
     */
    private function insert_or_change($isInsert, $hasAutoIncrement)
    {
        try {
            /** @var PDO $_ */
            [$_, $prepare] = $this->executedSql($this->sql, false);

            $time = microtime(true);
            if (($result = $prepare->execute()) === false) {
                throw new Exception($this->addErrorLog($prepare));
            }
            $this->dumpExecuteTime($time);
            if ($isInsert === true) {
                $result = $_->lastInsertId();
                if ($result == 0 && $hasAutoIncrement) {
                    throw new Exception($this->addErrorLog($prepare));
                }
            } else {
                $result = true;
            }
            $prepare->closeCursor();
            return $result;
        } catch (\Throwable $exception) {
            $result = $this->addError($this->sql . '. error: ' . $exception->getMessage());
            if (str_contains($exception->getMessage(), 'MySQL server has gone away')) {
                $this->db->refresh($this->sql);
                sleep(1);
                return $this->insert_or_change($isInsert, $hasAutoIncrement);
            }
            return $result;
        } finally {
            $this->db->release();
        }
    }


    private function dumpExecuteTime($time)
    {
        $time = round(microtime(true) - $time, 6);
        if ($time >= 0.03) {
            $request = request();
            if (!empty($request)) {
                # $this->debug($this->sql . '[' . $request->getUri() . '] Run-Time:' . $time);
            } else {
                # $this->debug($this->sql . ' Run-Time:' . $time);
            }
        }
    }

    /**
     * @param $modelName
     * @return $this
     */
    public function setModelName($modelName)
    {
        $this->_modelName = $modelName;
        return $this;
    }

    /**
     * @return string
     */
    public function getModelName()
    {
        return $this->_modelName;
    }

    /**
     * @return bool|int
     * @throws Exception
     */
    public function delete()
    {
        return $this->execute(static::EXECUTE);
    }

    /**
     * @return bool|int
     * @throws Exception
     */
    public function exec()
    {
        return $this->execute(static::EXECUTE);
    }

    /**
     * @param PDOStatement $prepare
     * @param $name
     * @param $value
     */
    public function bindParam($prepare, $name, $value)
    {
        $prepare->bindParam($name, $value);
    }

    /**
     * @param array|null $data
     * @return $this
     */
    public function bindValues(array $data = NULL)
    {
        if (!is_array($this->params)) {
            $this->params = [];
        }
        if (!empty($data)) {
            $this->params = array_merge($this->params, $data);
        }
        return $this;
    }

    /**
     * @param $sql
     * @return $this
     * @throws Exception
     */
    public function setSql($sql)
    {
        $this->sql = $sql;
        return $this;
    }

    /**
     * @return string
     */
    public function getError($prepare)
    {
        return $prepare->errorInfo()[2] ?? 'Db 驱动错误.';
    }

    /**
     * @return bool
     * @throws Exception
     */
    public function addErrorLog($prepare)
    {
        if ($prepare->errorCode() !== '00000') {
            return $this->addError($this->getError($prepare), 'mysql');
        }
        return true;
    }

    /**
     * @param PDOStatement $prepare
     * @return PDOStatement
     * @throws Exception
     */
    private function bind($prepare)
    {
        if (empty($this->params)) {
            return $prepare;
        }
        foreach ($this->params as $key => $val) {
            if (is_array($val)) {
                throw new Exception("Save data cannot have array");
            }
            $this->bindParam($prepare, ':' . ltrim($key, ':'), $val);
        }
        return $prepare;
    }
}
